package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
public class RedisIdWorker {
    private static final long BEGIN_TIMESTAMP = 1640995200L;

    // 序列号位数
    private static final int COUNT_BITS=32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix){
        // 全局id的构成：时间戳+序列号

        //序列号：由于Redis的自增操作是原子性的，保证了在并发情况下生成ID的唯一性，避免了传统数据库中的锁竞争和性能瓶颈。
        //因此我们可以利用Redis的自增原子性，让序列号由Redis自增的数值组成，因此我们确保了全局ID序列号的唯一性，从而确保了整个全局ID的唯一性。
        //同时，我们还需要考虑一个问题，我们利用Redis自增实现全局ID，但如果我们只设置一个Key值，随着业务的日积月累，自增值将会达到上限。
        //为避免这种情况发生，我们需要设置不同的Key值，于是我们决定用年月日的格式 yyyy:MM:dd 来添加到Key值的前缀当中，因此一个Key值的自增量不再是用来表示所有时间的业务量，而只是用来表示某年某月某天的业务量，而一天的业务量是不可能超过 2^32 (几十亿) 这么大的数值的，我们从而确保了Key值不会达到上限。
        //而这种做法也方便了我们对业务数据的统计，当我们想查询一年中的业务量时，我们只需要查询前缀为 yyyy 的Key值自增量即可，如果我们想查询某年某月的业务量时，我们只需要查询前缀为 yyyy:MM 的Key值自增量即可。

        //时间戳：为了增加全局ID的安全性，我们并能不直接把Redis的自增值(序列号)当作全局ID，而是应该在此基础上拼接一些其它信息，我们可以先设置某一个时间的时间戳作为参照时间戳，如2000年1月1日0时0分0秒，之后每当用户下单，我们可以获取下单时间的时间戳，再与参照时间戳做差，得到的差值用来组成全局ID的时间戳这一部分。
        // 显然，我们全局ID设置的时间戳只有32位，因此我们需要确保差值是在2^32大小内，而2^32秒相当于136年的时间，因此是妥妥够用的，或者我们也可以选择对参照时间差进行调整来确保差值不会超过2^32）

        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond-BEGIN_TIMESTAMP;

        // 2.生成序列号
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long increment = stringRedisTemplate.opsForValue().increment("icr:"+keyPrefix+":"+date);

        // 3.拼接并返回
        return timestamp<<COUNT_BITS | increment;
    }
    
//    public static void main(String[] args){
//        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
//        long second = time.toEpochSecond(ZoneOffset.UTC);
//        System.out.println("second="+second);
//    }
}
